"""Low-level helpers for GitLab credential management."""

import typing

from cki_lib import misc
from cki_lib.gitlab import get_instance
from cki_lib.gitlab import parse_gitlab_url
from gitlab import client
from gitlab.v4 import objects

from . import secrets
from . import utils


def create_personal_token(
    gl_instance: client.Gitlab,
    gl_owner: objects.Project | objects.Group,
    meta: dict[str, typing.Any],
) -> str:
    """Create personal access token and update meta information."""
    gl_personal_token = gl_owner.access_tokens.create({
        'name': meta['token_name'],
        'expires_at': (misc.now_tz_utc() + utils.DEFAULT_INTERVAL).date().isoformat(),
        'scopes': meta['scopes'],
        'access_level': meta['access_level'],
    })
    gl_user = gl_instance.users.get(gl_personal_token.user_id)
    meta.update({
        'token_id': gl_personal_token.id,
        'created_at': gl_personal_token.created_at,
        'expires_at': gl_personal_token.expires_at,
        'revoked': gl_personal_token.revoked,
        'user_id': gl_personal_token.user_id,
        'user_name': gl_user.username,
    })
    return typing.cast(str, gl_personal_token.token)


def create_deploy_token(
    gl_owner: objects.Project | objects.Group,
    meta: dict[str, typing.Any],
) -> str:
    """Create deploy token and update meta information."""
    gl_deploy_token = gl_owner.deploytokens.create({
        'name': meta['token_name'],
        'expires_at': meta.get('expires_at'),
        'scopes': meta['scopes'],
        # the '+' in the default gitlab+deploy-token-{n} breaks repo mirroring
        'username': misc.now_tz_utc().strftime('gitlab-deploy-token-%f'),
    })
    meta.update({
        'token_id': gl_deploy_token.id,
        'created_at': misc.now_tz_utc().isoformat(),
        'expires_at': gl_deploy_token.expires_at,
        'revoked': gl_deploy_token.revoked,
        'user_name': gl_deploy_token.username,
    })
    return typing.cast(str, gl_deploy_token.token)


def _self_token(instance_url: str, secret_token: str) -> objects.PersonalAccessToken:
    return get_instance(instance_url, token=secret_token).personal_access_tokens.get('self')


def update_project_token(meta: dict[str, str], secret_token: str) -> None:
    """Update meta information about project token."""
    gl_instance, gl_project = parse_gitlab_url(meta['project_url'])
    if 'token_id' not in meta:
        gl_self_token = _self_token(gl_instance.url, secret_token)
        meta['token_id'] = gl_self_token.id
    gl_project_token = gl_project.access_tokens.get(meta['token_id'])
    gl_member = gl_project.members.get(gl_project_token.user_id)
    meta.update({
        'scopes': gl_project_token.scopes,
        'access_level': gl_member.access_level,
        'token_name': gl_project_token.name,
        'token_id': gl_project_token.id,
        'created_at': gl_project_token.created_at,
        'expires_at': gl_project_token.expires_at,
        'revoked': gl_project_token.revoked,
        'active': gl_project_token.active,
        'user_id': gl_project_token.user_id,
        'user_name': gl_member.username,
    })


def update_group_token(meta: dict[str, str], secret_token: str) -> None:
    """Update meta information about group token."""
    gl_instance, gl_group = parse_gitlab_url(meta['group_url'])
    if 'token_id' not in meta:
        gl_self_token = _self_token(gl_instance.url, secret_token)
        meta['token_id'] = gl_self_token.id
    gl_group_token = gl_group.access_tokens.get(meta['token_id'])
    gl_member = gl_group.members.get(gl_group_token.user_id)
    meta.update({
        'scopes': gl_group_token.scopes,
        'access_level': gl_member.access_level,
        'token_name': gl_group_token.name,
        'token_id': gl_group_token.id,
        'created_at': gl_group_token.created_at,
        'expires_at': gl_group_token.expires_at,
        'revoked': gl_group_token.revoked,
        'active': gl_group_token.active,
        'user_id': gl_group_token.user_id,
        'user_name': gl_member.username,
    })


def update_personal_token(
    meta: dict[str, typing.Any],
    secret_token: str,
) -> None:
    """Update meta information about personal token."""
    if 'token_id' not in meta:
        gl_self_token = _self_token(meta['instance_url'], secret_token)
        meta['token_id'] = gl_self_token.id
        meta['user_id'] = gl_self_token.user_id
        meta['scopes'] = gl_self_token.scopes
    if set(meta['scopes']) & {'api', 'read_api'} and meta.get('active', True):
        gl_instance = get_instance(meta['instance_url'], token=secret_token)
    else:
        api_token = get_api_token_for_pat(meta, {'api', 'read_api'})
        gl_instance = get_instance(meta['instance_url'], token=secrets.secret(api_token))
    gl_personal_token = gl_instance.personal_access_tokens.get(meta['token_id'])
    gl_user = gl_instance.users.get(gl_personal_token.user_id)
    meta.update({
        'scopes': gl_personal_token.scopes,
        'token_name': gl_personal_token.name,
        'token_id': gl_personal_token.id,
        'created_at': gl_personal_token.created_at,
        'expires_at': gl_personal_token.expires_at,
        'revoked': gl_personal_token.revoked,
        'active': gl_personal_token.active,
        'user_id': gl_personal_token.user_id,
        'user_name': gl_user.username,
    })


def update_project_deploy_token(meta: dict[str, typing.Any]) -> None:
    """Update meta information about project deploy token."""
    _, gl_project = parse_gitlab_url(meta['project_url'])
    gl_deploy_token = gl_project.deploytokens.get(meta['token_id'])
    meta.update({
        'scopes': gl_deploy_token.scopes,
        'token_name': gl_deploy_token.name,
        'token_id': gl_deploy_token.id,
        'expires_at': gl_deploy_token.expires_at,
        'revoked': gl_deploy_token.revoked,
        'active': not gl_deploy_token.expired,
        'user_name': gl_deploy_token.username,
    })


def update_group_deploy_token(meta: dict[str, typing.Any]) -> None:
    """Update meta information about group deploy token."""
    _, gl_group = parse_gitlab_url(meta['group_url'])
    gl_deploy_token = gl_group.deploytokens.get(meta['token_id'])
    meta.update({
        'scopes': gl_deploy_token.scopes,
        'token_name': gl_deploy_token.name,
        'token_id': gl_deploy_token.id,
        'expires_at': gl_deploy_token.expires_at,
        'revoked': gl_deploy_token.revoked,
        'active': not gl_deploy_token.expired,
        'user_name': gl_deploy_token.username,
    })


def update_runner_token(meta: dict[str, typing.Any], secret_token: str) -> None:
    """Update meta information about runner token."""
    gl_instance = get_instance(meta['instance_url'])
    response = gl_instance.http_post(
        '/runners/verify', post_data={"token": secret_token})
    meta.update({
        'token_id': response['id'],
    })
    if expires_at := response['token_expires_at']:
        meta['expires_at'] = expires_at
        meta['active'] = misc.now_tz_utc() < misc.datetime_fromisoformat_tz_utc(expires_at)
    else:
        meta['active'] = True


def get_api_token_for_pat(
    pat_meta: dict[str, typing.Any],
    scopes: set[str],
) -> str:
    """Find a (preferably deployed) token with enough permissions to rotate the PAT."""
    return next(
        k for k, v in sorted(secrets.read_secrets_file().items(), reverse=True,
                             key=lambda i: misc.get_nested_key(i[1], 'meta/deployed', True))
        if misc.get_nested_key(v, 'meta/token_type') == pat_meta['token_type']
        and misc.get_nested_key(v, 'meta/instance_url') == pat_meta['instance_url']
        and misc.get_nested_key(v, 'meta/user_id') == pat_meta['user_id']
        and misc.get_nested_key(v, 'meta/active', True)
        and scopes & set(misc.get_nested_key(v, 'meta/scopes', []))
    )
